2 * Adium is the legal property of its developers, whose names are listed in the copyright file included
3 * with this source distribution.
5 * This program is free software; you can redistribute it and/or modify it under the terms of the GNU
6 * General Public License as published by the Free Software Foundation; either version 2 of the License,
7 * or (at your option) any later version.
9 * This program is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even
10 * the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General
11 * Public License for more details.
13 * You should have received a copy of the GNU General Public License along with this program; if not,
14 * write to the Free Software Foundation, Inc., 59 Temple Place - Suite 330, Boston, MA 02111-1307, USA.
16 #import <Adium/AIAccount.h>
17 #import <Adium/AIChat.h>
18 #import <Adium/AIContentMessage.h>
19 #import <Adium/AIListContact.h>
20 #import <Adium/ESFileTransfer.h>
21 #import <Adium/AIHTMLDecoder.h>
22 #import <Adium/AIServiceIcons.h>
23 #import <Adium/AIUserIcons.h>
25 #import <Adium/AIContactControllerProtocol.h>
26 #import <Adium/AIContentControllerProtocol.h>
27 #import <Adium/AIChatControllerProtocol.h>
28 #import <Adium/AIInterfaceControllerProtocol.h>
29 #import <Adium/AIPreferenceControllerProtocol.h>
31 #import <AIUtilities/AIArrayAdditions.h>
32 #import <AIUtilities/AIMutableOwnerArray.h>
34 #import "AIMessageWindowController.h"
35 #import "AIMessageWindow.h"
36 #import "AIInterfaceControllerProtocol.h"
37 #import "AIWebKitMessageViewController.h"
40 @interface AIChat (PRIVATE)
41 - (id)initForAccount:(AIAccount *)inAccount;
42 - (void)clearUniqueChatID;
43 - (void)clearListObjectStatuses;
46 @implementation AIChat
48 static int nextChatNumber = 0;
50 + (id)chatForAccount:(AIAccount *)inAccount
52 return [[[self alloc] initForAccount:inAccount] autorelease];
55 - (id)initForAccount:(AIAccount *)inAccount
57 if ((self = [super init])) {
59 account = [inAccount retain];
60 participatingListObjects = [[NSMutableArray alloc] init];
61 dateOpened = [[NSDate date] retain];
63 ignoredListContacts = nil;
67 customEmoticons = nil;
68 hasSentOrReceivedContent = NO;
69 pendingOutgoingContentObjects = [[NSMutableArray alloc] init];
71 AILog(@"[AIChat: %x initForAccount]",self);
82 AILog(@"[%@ dealloc]",self);
85 [participatingListObjects release];
87 [ignoredListContacts release];
88 [pendingOutgoingContentObjects release];
89 [uniqueChatID release]; uniqueChatID = nil;
90 [customEmoticons release]; customEmoticons = nil;
96 - (NSImage *)chatImage
98 AIListContact *listObject = [self listObject];
102 image = [[listObject parentContact] userIcon];
103 if (!image) image = [AIServiceIcons serviceIconForObject:listObject type:AIServiceIconLarge direction:AIIconNormal];
105 image = [AIServiceIcons serviceIconForObject:[self account] type:AIServiceIconLarge direction:AIIconNormal];
112 - (NSImage *)chatMenuImage
114 AIListObject *listObject;
115 NSImage *chatMenuImage = nil;
117 if ((listObject = [self listObject])) {
118 chatMenuImage = [AIUserIcons menuUserIconForObject:listObject];
121 return chatMenuImage;
125 //Associated Account ---------------------------------------------------------------------------------------------------
126 #pragma mark Associated Account
127 - (AIAccount *)account
132 - (void)setAccount:(AIAccount *)inAccount
134 if (inAccount != account) {
136 account = [inAccount retain];
138 //The uniqueChatID may depend upon the account, so clear it
139 [self clearUniqueChatID];
140 [[adium notificationCenter] postNotificationName:Chat_SourceChanged object:self]; //Notify
144 - (NSDictionary *)chatCreationDictionary
146 return [self statusObjectForKey:@"ChatCreationInfo"];
149 - (void)setChatCreationDictionary:(NSDictionary *)inDict
151 [self setStatusObject:inDict
152 forKey:@"ChatCreationInfo"
156 - (void)accountDidJoinChat
158 [self willChangeValueForKey:@"actionMenu"];
159 [self didChangeValueForKey:@"actionMenu"];
163 #pragma mark Date Opened
164 - (NSDate *)dateOpened
173 - (void)setIsOpen:(BOOL)flag
178 - (BOOL)hasSentOrReceivedContent
180 return hasSentOrReceivedContent;
182 - (void)setHasSentOrReceivedContent:(BOOL)flag
184 hasSentOrReceivedContent = flag;
187 //Status ---------------------------------------------------------------------------------------------------------------
190 - (void)didModifyStatusKeys:(NSSet *)keys silent:(BOOL)silent
192 [[adium chatController] chatStatusChanged:self
193 modifiedStatusKeys:keys
197 - (void)object:(id)inObject didSetStatusObject:(id)value forKey:(NSString *)key notify:(NotifyTiming)notify
199 //If our unviewed content changes or typing status changes, and we have a single list object,
200 //apply the change to that object as well so it can be cleanly reflected in the contact list.
201 if ([key isEqualToString:KEY_UNVIEWED_CONTENT] ||
202 [key isEqualToString:KEY_TYPING]) {
203 AIListObject *listObject = [self listObject];
205 if (listObject) [listObject setStatusObject:value forKey:key notify:notify];
208 [super object:inObject didSetStatusObject:value forKey:key notify:notify];
211 - (void)clearListObjectStatuses
213 AIListObject *listObject = [self listObject];
216 [listObject setStatusObject:nil forKey:KEY_UNVIEWED_CONTENT notify:NotifyLater];
217 [listObject setStatusObject:nil forKey:KEY_TYPING notify:NotifyLater];
219 [listObject notifyOfChangedStatusSilently:NO];
223 //Secure chatting ------------------------------------------------------------------------------------------------------
224 - (void)setSecurityDetails:(NSDictionary *)securityDetails
226 [self setStatusObject:securityDetails
227 forKey:@"SecurityDetails"
230 - (NSDictionary *)securityDetails
232 return [self statusObjectForKey:@"SecurityDetails"];
237 AIEncryptionStatus encryptionStatus = [self encryptionStatus];
239 return (encryptionStatus != EncryptionStatus_None);
242 - (AIEncryptionStatus)encryptionStatus
244 AIEncryptionStatus encryptionStatus = EncryptionStatus_None;
246 NSDictionary *securityDetails = [self securityDetails];
247 if (securityDetails) {
248 NSNumber *detailsStatus;
249 if ((detailsStatus = [securityDetails objectForKey:@"EncryptionStatus"])) {
250 encryptionStatus = [detailsStatus intValue];
253 /* If we don't have a specific encryption status, but do have security details, assume
254 * encrypted and verified.
256 encryptionStatus = EncryptionStatus_Verified;
260 return encryptionStatus;
263 - (BOOL)supportsSecureMessagingToggling
265 return (BOOL)[account allowSecureMessagingTogglingForChat:self];
268 //Name ----------------------------------------------------------------------------------------------------------------
274 - (void)setName:(NSString *)inName
276 if (name != inName) {
277 [name release]; name = [inName retain];
282 * @brief Return an identifier which can be used to look up this chat later
284 * Use setIdentifier to specify an arbitrary identifier for this chat.
286 * Use uniqueChatID as a unique identifier for a contact-service combination.
294 * @brief Set an identifier for this chat
296 * Only an account which created a chat should specify the identifier; it has no useful menaing outside that context.
298 - (void)setIdentifier:(id)inIdentifier
300 if (identifier != inIdentifier) {
301 [identifier release];
302 identifier = [inIdentifier retain];
306 - (NSString *)displayName
308 NSString *outName = [self displayArrayObjectForKey:@"Display Name"];
309 return outName ? outName : (name ? name : [[self listObject] displayName]);
312 - (void)setDisplayName:(NSString *)inDisplayName
314 [[self displayArrayForKey:@"Display Name"] setObject:inDisplayName
318 //Participating ListObjects --------------------------------------------------------------------------------------------
319 #pragma mark Participating ListObjects
321 - (void)addParticipatingListObject:(AIListContact *)inObject notify:(BOOL)notify
323 if (![participatingListObjects containsObjectIdenticalTo:inObject]) {
325 [participatingListObjects addObject:inObject];
327 [[adium chatController] chat:self addedListContact:inObject notify:notify];
331 // Invite a list object to join the chat. Returns YES if the chat joins, NO otherwise
332 - (BOOL)inviteListContact:(AIListContact *)inContact withMessage:(NSString *)inviteMessage
334 return ([[self account] inviteContact:inContact toChat:self withMessage:inviteMessage]);
337 - (void)setPreferredListObject:(AIListContact *)inObject
339 preferredListObject = inObject;
342 - (AIListContact *)preferredListObject
344 return preferredListObject;
347 //If this chat only has one participating list object, it is returned. Otherwise, nil is returned
348 - (AIListContact *)listObject
350 if (([participatingListObjects count] == 1) && ![self isGroupChat]) {
351 return [participatingListObjects objectAtIndex:0];
356 - (void)setListObject:(AIListContact *)inListObject
358 if (inListObject != [self listObject]) {
359 if ([participatingListObjects count]) {
360 [participatingListObjects removeObjectAtIndex:0];
362 [self addObject:inListObject];
364 //Clear any local caches relying on the list object
365 [self clearListObjectStatuses];
366 [self clearUniqueChatID];
368 //Notify once the destination has been changed
369 [[adium notificationCenter] postNotificationName:Chat_DestinationChanged object:self];
373 - (NSString *)uniqueChatID
376 if ([self isGroupChat]) {
377 uniqueChatID = [[NSString alloc] initWithFormat:@"%@.%i",[self name],nextChatNumber++];
379 uniqueChatID = [[[self listObject] internalObjectID] retain];
383 uniqueChatID = [[NSString alloc] initWithFormat:@"UnknownChat.%i",nextChatNumber++];
384 NSLog(@"Warning: Unknown chat %p",self);
388 return (uniqueChatID);
391 - (void)clearUniqueChatID
393 [uniqueChatID release]; uniqueChatID = nil;
396 //Content --------------------------------------------------------------------------------------------------------------
400 * @brief Informs the chat that the core and the account are ready to begin filtering and sending a content object
402 * If there is only one object in pendingOutgoingContentObjects after adding inObject, we should send immedaitely.
403 * However, if other objects are in it, we should wait for them to be removed, as they are chronologically first.
404 * If we are asked if we should begin sending the earliest object in pendingOutgoingContentObjects, the answer is YES.
406 * @param inObject The object being sent
407 * @result YES if the object should be sent immediately; NO if another object is in process so we should wait
409 - (BOOL)willBeginSendingContentObject:(AIContentObject *)inObject
411 int currentIndex = [pendingOutgoingContentObjects indexOfObjectIdenticalTo:inObject];
413 //Don't add the object twice when we are called from -[AIChat finishedSendingContentObject]
414 if (currentIndex == NSNotFound) {
415 [pendingOutgoingContentObjects addObject:inObject];
418 return (([pendingOutgoingContentObjects count] == 1) ||
419 (currentIndex == 0));
423 * @brief Informs the chat that an outgoing content object was sent and dispalyed.
425 * It is no longer pending, so we remove it from that array.
426 * If there are more pending objects, trigger sending the next.
428 * @param inObject The object with which we are finished
430 - (void)finishedSendingContentObject:(AIContentObject *)inObject
432 [pendingOutgoingContentObjects removeObjectIdenticalTo:inObject];
434 if ([pendingOutgoingContentObjects count]) {
435 [[adium contentController] sendContentObject:[pendingOutgoingContentObjects objectAtIndex:0]];
439 - (BOOL)canSendMessages
441 BOOL canSendMessages;
442 if ([self isGroupChat]) {
444 canSendMessages = YES;
447 if ([[self account] online]) {
448 AIListContact *listObject = [self listObject];
450 canSendMessages = ([listObject online] ||
451 [listObject isStranger] ||
452 [[self account] canSendOfflineMessageToContact:listObject]);
454 canSendMessages = NO;
458 return canSendMessages;
461 - (BOOL)canSendImages
463 return [[self account] canSendImagesForChat:self];
466 - (int)unviewedContentCount
468 return [self integerStatusObjectForKey:KEY_UNVIEWED_CONTENT];
471 - (void)incrementUnviewedContentCount
473 int currentUnviewed = [self integerStatusObjectForKey:KEY_UNVIEWED_CONTENT];
474 [self setStatusObject:[NSNumber numberWithInt:(currentUnviewed+1)]
475 forKey:KEY_UNVIEWED_CONTENT
479 - (void)clearUnviewedContentCount
481 [self setStatusObject:nil forKey:KEY_UNVIEWED_CONTENT notify:NotifyNow];
484 #pragma mark AIContainingObject protocol
485 //AIContainingObject protocol
486 - (NSArray *)containedObjects
488 return participatingListObjects;
491 - (unsigned)containedObjectsCount
493 return [[self containedObjects] count];
496 - (BOOL)containsObject:(AIListObject *)inObject
498 return [[self containedObjects] containsObjectIdenticalTo:inObject];
501 - (id)objectAtIndex:(unsigned)index
503 return [[self containedObjects] objectAtIndex:index];
506 - (int)indexOfObject:(AIListObject *)inObject
508 return [[self containedObjects] indexOfObject:inObject];
511 //Retrieve a specific object by service and UID
512 - (AIListObject *)objectWithService:(AIService *)inService UID:(NSString *)inUID
514 NSEnumerator *enumerator = [[self containedObjects] objectEnumerator];
515 AIListObject *object;
517 while ((object = [enumerator nextObject])) {
518 if ([inUID isEqualToString:[object UID]] && [object service] == inService) break;
524 - (NSArray *)listContacts
526 return [self containedObjects];
529 - (BOOL)addObject:(AIListObject *)inObject
531 if ([inObject isKindOfClass:[AIListContact class]]) {
532 [self addParticipatingListObject:(AIListContact *)inObject notify:YES];
540 - (void)removeObject:(AIListObject *)inObject
542 if ([inObject isKindOfClass:[AIListContact class]] && [participatingListObjects containsObjectIdenticalTo:inObject]) {
543 [participatingListObjects removeObject:inObject];
545 [[adium chatController] chat:self removedListContact:(AIListContact *)inObject];
549 - (void)removeAllObjects
551 while([self containedObjectsCount] > 0)
552 [self removeObject:[self objectAtIndex:0]];
555 - (void)setExpanded:(BOOL)inExpanded
557 expanded = inExpanded;
564 - (unsigned)visibleCount
566 return [self containedObjectsCount];
569 - (NSString *)contentsBasedIdentifier
571 return [NSString stringWithFormat:@"%@-%@.%@",[self name], [[self account] serviceID], [[self account] UID]];
576 - (float)smallestOrder { return 0; }
577 - (float)largestOrder { return 1E10; }
578 - (void)listObject:(AIListObject *)listObject didSetOrderIndex:(float)inOrderIndex {};
583 * @brief Set the ignored state of a contact
585 * @param inContact The contact whose state is to be changed
586 * @param isIgnored YES to ignore the contact; NO to not ignore the contact
588 - (void)setListContact:(AIListContact *)inContact isIgnored:(BOOL)isIgnored
590 //Create ignoredListContacts if needed
591 if (isIgnored && !ignoredListContacts) {
592 ignoredListContacts = [[NSMutableSet alloc] init];
596 [ignoredListContacts addObject:inContact];
598 [ignoredListContacts removeObject:inContact];
603 * @brief Is the passed object ignored?
605 * @param inContact The contact to check
606 * @result YES if the contact is ignored; NO if it is not
608 - (BOOL)isListContactIgnored:(AIListObject *)inContact
610 return [ignoredListContacts containsObject:inContact];
613 #pragma mark Comparison
614 - (BOOL)isEqual:(id)inChat
616 return (inChat == self);
619 #pragma mark Debugging
620 - (NSString *)description
622 return [NSString stringWithFormat:@"%@:%@",
624 (uniqueChatID ? uniqueChatID : @"<new>")];
627 #pragma mark Group Chat
629 - (void)setIsGroupChat:(BOOL)flag
639 #pragma mark Custom emoticons
641 - (void)addCustomEmoticon:(AIEmoticon *)inEmoticon
643 if (!customEmoticons) customEmoticons = [[NSMutableSet alloc] init];
644 [customEmoticons addObject:inEmoticon];
647 - (NSSet *)customEmoticons;
649 return customEmoticons;
655 * @brief Inform the chat that an error occurred
657 * @param type An NSNumber containing an AIChatErrorType
659 - (void)receivedError:(NSNumber *)type
662 [self setStatusObject:type forKey:KEY_CHAT_ERROR notify:NotifyNow];
664 //No need to continue to store the NSNumber
665 [self setStatusObject:nil forKey:KEY_CHAT_ERROR notify:NotifyNever];
668 #pragma mark Room commands
669 - (NSMenu *)actionMenu
671 return [[self account] actionsForChat:self];
673 - (void)setActionMenu:(NSMenu *)inMenu {};
675 #pragma mark Applescirpt
677 - (NSScriptObjectSpecifier *)objectSpecifier
679 //the chat may not be in a window! Just reference it from the application...
681 NSScriptClassDescription *containerClassDesc = (NSScriptClassDescription *)[NSScriptClassDescription classDescriptionForClass:[NSApp class]];
682 return [[[NSUniqueIDSpecifier allocWithZone:[self zone]]
683 initWithContainerClassDescription:containerClassDesc
684 containerSpecifier:nil key:@"chats" uniqueID:[self uniqueChatID]] autorelease];
687 - (unsigned int)index
689 id<AIChatContainer> messageTab = [self statusObjectForKey:@"MessageTabViewItem"];
690 //what we're going to do is find this tab in the tab view's hierarchy, so as to get its index
691 id<AIChatWindowController> windowController = [messageTab windowController];
693 NSArray *chats = [windowController containedChats];
694 for (unsigned int i=0;i<[chats count];i++) {
695 if ([chats objectAtIndex:i] == self)
696 return i+1; //one based
698 NSAssert(NO, @"This chat is weird.");
701 /*- (void)setIndex:(unsigned int)index
703 id<AIChatContainer> messageTab = [self statusObjectForKey:@"MessageTabViewItem"];
704 id<AIChatWindowController> windowController = [messageTab windowController];
705 NSArray *chats = [windowController containedChats];
706 NSAssert (index-1 < [chats count], @"Don't let index be bigger than the count!");
707 NSLog(@"Trying to move %@ in %@ to %u",messageTab,window,index-1);
708 [windowController moveTabViewItem:messageTab toIndex:index-1]; //This is bad bad bad. Why?
714 id<AIChatContainer> messageTab = [self statusObjectForKey:@"MessageTabViewItem"];
715 id<AIChatWindowController> windowController = [messageTab windowController];
716 return [windowController window];
719 - (id)handleCloseScriptCommand:(NSCloseCommand *)closeCommand
721 [[[AIObject sharedAdiumInstance] interfaceController] closeChat:self];
725 - (void)setUniqueChatID:(NSString *)str
727 [[NSScriptCommand currentCommand] setScriptErrorNumber:errOSACantAssign];
730 - (AIAccount *)scriptingAccount
732 return [self account];
735 - (void)setScriptingAccount:(AIAccount *)a
737 [[NSScriptCommand currentCommand] setScriptErrorNumber:errOSACantAssign];
738 [[NSScriptCommand currentCommand] setScriptErrorString:@"Can't set the account of a chat."];
741 - (NSString *)content
743 /*AITranscriptLogEnumerator *e = [[[AITranscriptLogReader alloc] initWithChat:self] autorelease];
745 NSMutableString *result = [[[NSMutableString alloc] init] autorelease];
746 while ((m = [e nextObject])) {
747 [result appendFormat:@"%@\n",[m messageString]];
750 [[NSScriptCommand currentCommand] setScriptErrorNumber:errOSACantAssign];
751 [[NSScriptCommand currentCommand] setScriptErrorString:@"Still unsupported."];
756 * @brief Applescript command to send a message in this chat
758 - (id)sendScriptCommand:(NSScriptCommand *)command {
759 NSDictionary *evaluatedArguments = [command evaluatedArguments];
760 NSString *message = [evaluatedArguments objectForKey:@"message"];
761 NSURL *fileURL = [evaluatedArguments objectForKey:@"withFile"];
763 //Send any message we were told to send
764 if (message && [message length]) {
765 //Take the string and turn it into an attributed string (in case we were passed HTML)
766 NSAttributedString *attributedMessage = [AIHTMLDecoder decodeHTML:message];
767 AIContentMessage *messageContent;
768 messageContent = [AIContentMessage messageInChat:self
769 withSource:[self account]
770 destination:[self listObject]
772 message:attributedMessage
775 [[adium contentController] sendContentObject:messageContent];
778 //Send any file we were told to send to every participating list object (anyone remember the AOL mass mailing zareW scene?)
779 if (fileURL && [[fileURL path] length]) {
780 NSEnumerator *enumerator = [[self containedObjects] objectEnumerator];
781 AIListContact *listContact;
783 while ((listContact = [enumerator nextObject])) {
784 AIListContact *targetFileTransferContact;
786 //Make sure we know where we are sending the file by finding the best contact for
787 //sending CONTENT_FILE_TRANSFER_TYPE.
788 targetFileTransferContact = [[adium contactController] preferredContactForContentType:CONTENT_FILE_TRANSFER_TYPE
789 forListContact:listContact];
790 [[adium fileTransferController] sendFile:[fileURL path]
791 toListContact:targetFileTransferContact];